package common;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * BitManager.java
 * 
 * This class is used to hold an arbitrarily long chunk of bits. Bits may be
 * added or retrieved from the data block in arbitrary lengths. This allows
 * for efficient compression of an output file. 
 * 
 * @author Zachary M. Allen
 */
public class BitManager {

  /**
   * Current position in file
   */
  private int position;

  /**
   * The data array that holds the data chunk
   */
  private char[] bitmask;

  /**
   * Current size of bitmask
   */
  private int size;

  /**
   * Constructor
   *
   */
  public BitManager() {
    position = 0;
    bitmask = new char[1];
    size = bitmask.length;
  }

  /**
   * Adds data to the data block - chains to void add(long, int)
   * @param    data    Data to add
   * @param    numBits    Number of bits to use to represent the new data
   */
  public void addBits(int data, int numBits) {
    addBits((long) data, numBits);
  }

  /**
   * Adds data to the data block
   * @param    data    Data to add
   * @param    numBits    Number of bits to use to represent the new data
   */
  public void addBits(long data, int numBits) {
    if ((numBits + position) >= (8 * bitmask.length)) {
      bitmask = resizeCharArray(bitmask, numBits + position);
    }

    long shift = numBits - 1;
    for (int i = position; i < position + numBits; i++) {
      int index = i / 8;
      long bit = ((data >> shift--) & 1);
      bitmask[index] = (char) ((bitmask[index]) | bit << (7 - (i % 8)));
    }

    position += numBits;
    size = 1 + (position / 8);

  }
  
  /**
   * Resizes a char array when necessary
   * @param    charArray    Char array to resize
   * @param    newSize    New size of array
   */
  private char[] resizeCharArray(char[] charArray, int newSize) {
    char[] newCharArray = new char[newSize];
    for (int i = 0; i < charArray.length; i++) {
      newCharArray[i] = charArray[i];
    }
    return newCharArray;
  }

  /**
   * Retrieves data bits from the block
   * @param    firstBit    Position of first bit to retrieve
   * @param    numBits    Number of bits to retrieve
   * @return    Requested bits from the data block
   */
  public long getBits(int firstBit, int numBits) {

    long retval = 0;

    for (int i = firstBit; i < firstBit + numBits; i++) {
      int index = i / 8;
      if (index >= bitmask.length) {
        return 0;
      }
      long bit = (bitmask[index] >> (7 - (i % 8))) & 1;
      retval = (retval << 1) | bit;
    }

    return retval;

  }

  /**
   * Loads a file for dumping
   * @param    file    File to load
   * @throws    IOException
   */
  public void load(String file) throws IOException {
    File f = new File(file);
    BufferedInputStream fis = new BufferedInputStream(new FileInputStream(f));
    bitmask = new char[(int) f.length()];
    for (int i = 0; i < f.length(); i++) {
      bitmask[i] = (char) fis.read();
    }
    fis.close();
  }

  /**
   * Saves the data block to an output file
   * @param    file    File to save
   * @return the absolute path the file was saved to
   * @throws    IOException
   */
  public String save(String file) throws IOException {
    BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
    for (int i = 0; i < size; i++) {
      fos.write(bitmask[i]);
    }
    fos.close();
    return new File(file).getAbsolutePath();
  }

}